//
//  WCTextFile.m
//  WabbitCode
//
//  Created by William Towe on 1/23/09.
//  Copyright 2009 Revolution Software. All rights reserved.
//

#import "WCTextFile.h"
#import "NSImage+WCExtensions.h"
#import "WCProject.h"
#import "NSTreeController+WCExtensions.h"
#import "NSFileManager+WCExtensions.h"
#import "NSTabView+WCExtensions.h"
#import "WCMacros.h"
#import "NSDictionary+WCExtensions.h"
#import "WCProjectGroup.h"
#import "NSString+WCExtensions.h"
#import "WCAppController.h"
#import "WCBreakpoint.h"
#import "WCSymbol.h"
#import "WCSyntaxHighlighter.h"
#import "WCInterfacePerformer.h"
#import <RegexKit/RegexKit.h>
#import "WCBuildTargetsGroup.h"
#import "WCBuildConfiguration.h"
#import "NSAttributedString+WCExtensions.h"


#define kWCTFFileEncodingKey @"fileEncoding"
#define kWCTFFileBreakpointMarkersKey @"fileBreakpointMarkers"
#define kWCTFFileSelectedRangeKey @"fileSelectedRange"
#define kWCTFSeparateEditorWindowFrameString @"separateEditorWindowFrameString"

NSString *kWCTFFileChangeCountDidChangeNotification = @"kWCTFFileChangeCountDidChangeNotification";
NSString *kWCTFFileNumberOfErrorsDidChangeNotification = @"kWCTFFileNumberOfErrorsDidChangeNotification";
NSString *kWCTFFileNumberOfWarningsDidChangeNotification = @"kWCTFFileNumberOfWarningsDidChangeNotification";
NSString *kWCTFFileNumberOfBreakpointsDidChangeNotification = @"kWCTFFileNumberOfBreakpointsDidChangeNotification";
NSString *kWCTFFileStatusOfBreakpointsDidChangeNotification = @"kWCTFFileStatusOfBreakpointsDidChangeNotification";

static NSMutableDictionary *fileExtensionsToUnsavedFileIcons = nil;
static NSCharacterSet *whiteSpaceCharacterSet = nil;
static NSCharacterSet *equateCharacterSet = nil;

void yystatereset();
void yy_scan_string(const char *str);
unsigned int yylex();

@interface WCTextFile (Private)
- (void)_calculateLines;
- (void)_calculateLinesStartingAtLine:(unsigned)line;
@end

@implementation WCTextFile
#pragma mark *** Subclass Overrides ***
+ (void)initialize {
	fileExtensionsToUnsavedFileIcons = [[NSMutableDictionary alloc] init];
	whiteSpaceCharacterSet = [[NSCharacterSet whitespaceCharacterSet] retain];
	
	NSMutableCharacterSet *temp = [NSMutableCharacterSet characterSetWithCharactersInString:@"="];
	[temp formUnionWithCharacterSet:whiteSpaceCharacterSet];
	equateCharacterSet = [temp copy];
}

+ (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key {
	if ([key isEqualToString:@"isEdited"])
		return [NSSet setWithObject:@"fileChangeCount"];
	return [super keyPathsForValuesAffectingValueForKey:key];
}

- (void)dealloc {
	[_fileAttributes release];
	[_fileUndoManager release];
	[_fileBreakpointMarkers release];
	[_fileErrorLineNumbers release];
	[_fileWarningLineNumbers release];
	[_fileTextStorage release];
	[_lineStartIndexes release];
	[_fileLabelsArray release];
	[_fileEquatesArray release];
	[_fileSymbolsArray release];
	[_fileLabels release];
	[_fileEquates release];
	[_fileMacros release];
	[_fileDefines release];
	[_scrollViewWithTextViewForReplace release];
	[_separateEditorWindowFrameString release];
	[super dealloc];
}

- (NSImage *)icon {
	if ([self isEdited]) {
		// we cache the unsaved icon images here because otherwise it slows down the PSMTabBarControl when dragging and dropping tabs, since it tries to create these unsaved icons repeatedly as the tab items are being dragged
		NSString *fileExtension = [self fileExtension];
		NSImage *unsavedIcon = [fileExtensionsToUnsavedFileIcons objectForKey:fileExtension];
		if (unsavedIcon == nil) {
			unsavedIcon = [[super icon] unsavedIconFromImage];
			[fileExtensionsToUnsavedFileIcons setObject:unsavedIcon forKey:fileExtension];
		}
		return unsavedIcon;
	}
	return [super icon];
}

- (Class)commonClass {
	return [self superclass];
}
#pragma mark NSTextView Delegate
- (NSUndoManager *)undoManagerForTextView:(NSTextView *)textView {
	return [self fileUndoManager];
}
#pragma mark NSCoding Protocol
- (id)initWithCoder:(NSCoder *)coder {
	if (!(self = [super initWithCoder:coder]))
		return nil;
	
	_fileEncoding = [coder decodeIntegerForKey:kWCTFFileEncodingKey];
	_fileBreakpointMarkers = [[coder decodeObjectForKey:kWCTFFileBreakpointMarkersKey] retain];
	_fileSelectedRange = NSRangeFromString([coder decodeObjectForKey:kWCTFFileSelectedRangeKey]);
	_separateEditorWindowFrameString = [[coder decodeObjectForKey:kWCTFSeparateEditorWindowFrameString] retain];
	//_fileErrorLineNumbers = [[coder decodeObjectForKey:@"errorLineNumbers"] retain];
	//_fileWarningLineNumbers = [[coder decodeObjectForKey:@"warningLineNumbers"] retain];
	
	_fileLabels = [[NSMutableSet alloc] init];
	_fileEquates = [[NSMutableSet alloc] init];
	_fileMacros = [[NSMutableSet alloc] init];
	_fileDefines = [[NSMutableSet alloc] init];
	_fileLabelsArray = [[NSMutableArray alloc] init];
	_fileEquatesArray = [[NSMutableArray alloc] init];
	_fileSymbolsArray = [[NSMutableArray alloc] init];
	
	_WCTextFileFlags.rebuildFileSymbols = YES;
	
	return self;
}

- (void)encodeWithCoder:(NSCoder *)coder {
	[super encodeWithCoder:coder];
	[coder encodeInteger:_fileEncoding forKey:kWCTFFileEncodingKey];
	[coder encodeObject:_fileBreakpointMarkers forKey:kWCTFFileBreakpointMarkersKey];
	[coder encodeObject:NSStringFromRange(_fileSelectedRange) forKey:kWCTFFileSelectedRangeKey];
	[coder encodeObject:_separateEditorWindowFrameString forKey:kWCTFSeparateEditorWindowFrameString];
	//[coder encodeObject:_fileErrorLineNumbers forKey:@"errorLineNumbers"];
	//[coder encodeObject:_fileWarningLineNumbers forKey:@"warningLineNumbers"];
}
#pragma mark NSCopying Protocol
- (id)copyWithZone:(NSZone *)zone {
	WCTextFile *copy = [super copyWithZone:zone];
	
	copy->_fileEncoding = _fileEncoding;
	copy->_fileChangeCount = _fileChangeCount;
	copy->_fileUndoManager = [_fileUndoManager retain];
	copy->_fileBreakpointMarkers = [_fileBreakpointMarkers retain];
	copy->_fileErrorLineNumbers = [_fileErrorLineNumbers retain];
	copy->_fileWarningLineNumbers = [_fileWarningLineNumbers retain];
	copy->_fileTextStorage = [_fileTextStorage retain];
	
	return copy;
}
#pragma mark *** Public Methods ***
- (NSUInteger)lineNumberForCharacterIndex:(NSUInteger)index; {
	if (!_lineStartIndexes)
		[self _calculateLines];
	
	unsigned			left, right, mid, lineStart;
	NSMutableArray		*lines;
	
	lines = _lineStartIndexes;
	
    // Binary search
    left = 0;
    right = [lines count];
	
    while ((right - left) > 1)
    {
        mid = (right + left) / 2;
        lineStart = [[lines objectAtIndex:mid] unsignedIntValue];
        
        if (index < lineStart)
        {
            right = mid;
        }
        else if (index > lineStart)
        {
            left = mid;
        }
        else
        {
            return mid;
        }
    }
    return left;
}
- (NSUInteger)lineStartIndexForLineNumber:(NSUInteger)lineNumber; {
	if (!_lineStartIndexes)
		[self _calculateLines];
	
	return [[_lineStartIndexes objectAtIndex:lineNumber] unsignedIntegerValue];
}
#pragma mark Breakpoints
- (WCBreakpoint *)addFileBreakpointAtLineNumber:(NSUInteger)ln; {
	if (!_fileBreakpointMarkers) {
		_fileBreakpointMarkers = [[NSMutableDictionary alloc] init];
	}
	WCBreakpoint *breakpoint = [WCBreakpoint breakpointEnabled];
	[_fileBreakpointMarkers setObject:breakpoint forKey:[NSNumber numberWithUnsignedInteger:ln]];
	
	[[NSNotificationCenter defaultCenter] postNotificationName:kWCTFFileNumberOfBreakpointsDidChangeNotification object:self];
	
	return breakpoint;
}
- (void)addFileBreakpoint:(WCBreakpoint *)bp atLineNumber:(NSUInteger)ln; {
	[_fileBreakpointMarkers setObject:bp forKey:[NSNumber numberWithUnsignedInteger:ln]];
	
	[[NSNotificationCenter defaultCenter] postNotificationName:kWCTFFileNumberOfBreakpointsDidChangeNotification object:self];
}
- (void)removeFileBreakpointAtLineNumber:(NSUInteger)ln; {
	[_fileBreakpointMarkers removeObjectForKey:[NSNumber numberWithUnsignedInteger:ln]];
	
	[[NSNotificationCenter defaultCenter] postNotificationName:kWCTFFileNumberOfBreakpointsDidChangeNotification object:self];
}
- (WCBreakpoint *)fileBreakpointAtLineNumber:(NSUInteger)ln; {
	return [_fileBreakpointMarkers objectForKey:[NSNumber numberWithUnsignedInteger:ln]];
}
- (void)toggleBreakpointAtLineNumber:(NSUInteger)ln; {
	WCBreakpoint *breakpoint = [self fileBreakpointAtLineNumber:ln];
	if (!breakpoint)
		return;
	
	[breakpoint setEnabled:![breakpoint isEnabled]];
	
	[[NSNotificationCenter defaultCenter] postNotificationName:kWCTFFileStatusOfBreakpointsDidChangeNotification object:self];
}
- (void)toggleBreakpoint:(WCBreakpoint *)bp; {
	[bp setEnabled:![bp isEnabled]];
	
	[[NSNotificationCenter defaultCenter] postNotificationName:kWCTFFileStatusOfBreakpointsDidChangeNotification object:self];
}
- (NSUInteger)numberOfFileBreakpoints; {
	return [_fileBreakpointMarkers count];
}
- (void)enableAllFileBreakpoints; {
	for (WCBreakpoint *bp in [_fileBreakpointMarkers allValues]) {
		[bp setEnabled:YES];
	}
	
	[[NSNotificationCenter defaultCenter] postNotificationName:kWCTFFileStatusOfBreakpointsDidChangeNotification object:self];
}
- (void)disableAllFileBreakpoints; {
	for (WCBreakpoint *bp in [_fileBreakpointMarkers allValues]) {
		[bp setEnabled:NO];
	}
	
	[[NSNotificationCenter defaultCenter] postNotificationName:kWCTFFileStatusOfBreakpointsDidChangeNotification object:self];
}
- (void)removeAllFileBreakpoints; {
	[_fileBreakpointMarkers removeAllObjects];
	
	[[NSNotificationCenter defaultCenter] postNotificationName:kWCTFFileNumberOfBreakpointsDidChangeNotification object:self];
}
#pragma mark Errors
- (void)addFileError:(WCFileError *)fileError atLineNumber:(NSUInteger)ln; {
	if (!_fileErrorLineNumbers)
		_fileErrorLineNumbers = [[NSMutableDictionary alloc] init];
	[self willChangeValueForKey:@"numberOfFileErrors"];
	[_fileErrorLineNumbers setObject:fileError forKey:[NSNumber numberWithUnsignedInteger:ln]];
	[self didChangeValueForKey:@"numberOfFileErrors"];
	
	[[NSNotificationCenter defaultCenter] postNotificationName:kWCTFFileNumberOfErrorsDidChangeNotification object:self];
}
- (void)removeFileErrorAtLineNumber:(NSUInteger)ln; {
	[self willChangeValueForKey:@"numberOfFileErrors"];
	[_fileErrorLineNumbers removeObjectForKey:[NSNumber numberWithUnsignedInteger:ln]];
	[self didChangeValueForKey:@"numberOfFileErrors"];
	
	[[NSNotificationCenter defaultCenter] postNotificationName:kWCTFFileNumberOfErrorsDidChangeNotification object:self];
}
- (void)removeAllFileErrors; {
	[self willChangeValueForKey:@"numberOfFileErrors"];
	[_fileErrorLineNumbers removeAllObjects];
	[self didChangeValueForKey:@"numberOfFileErrors"];
	
	[[NSNotificationCenter defaultCenter] postNotificationName:kWCTFFileNumberOfErrorsDidChangeNotification object:self];
}
- (BOOL)containsFileErrorAtLineNumber:(NSUInteger)ln; {
	return ([_fileErrorLineNumbers objectForKey:[NSNumber numberWithUnsignedInteger:ln]] != nil);
}
- (WCFileError *)fileErrorAtLineNumber:(NSUInteger)ln; {
	return [_fileErrorLineNumbers objectForKey:[NSNumber numberWithUnsignedInteger:ln]];
}
#pragma mark Warnings
- (void)addFileWarning:(WCFileWarning *)fileWarning atLineNumber:(NSUInteger)ln; {
	if (!_fileWarningLineNumbers)
		_fileWarningLineNumbers = [[NSMutableDictionary alloc] init];

	[self willChangeValueForKey:@"numberOfFileWarnings"];
	[_fileWarningLineNumbers setObject:fileWarning forKey:[NSNumber numberWithUnsignedInteger:ln]];
	[self didChangeValueForKey:@"numberOfFileWarnings"];

	
	[[NSNotificationCenter defaultCenter] postNotificationName:kWCTFFileNumberOfWarningsDidChangeNotification object:self];
}
- (void)removeFileWarningAtLineNumber:(NSUInteger)ln; {

	[self willChangeValueForKey:@"numberOfFileWarnings"];
	[_fileWarningLineNumbers removeObjectForKey:[NSNumber numberWithUnsignedInteger:ln]];
	[self didChangeValueForKey:@"numberOfFileWarnings"];

	
	[[NSNotificationCenter defaultCenter] postNotificationName:kWCTFFileNumberOfWarningsDidChangeNotification object:self];
}
- (void)removeAllFileWarnings; {

	[self willChangeValueForKey:@"numberOfFileWarnings"];
	[_fileWarningLineNumbers removeAllObjects];
	[self didChangeValueForKey:@"numberOfFileWarnings"];

	
	[[NSNotificationCenter defaultCenter] postNotificationName:kWCTFFileNumberOfWarningsDidChangeNotification object:self];
}
- (BOOL)containsFileWarningAtLineNumber:(NSUInteger)ln; {
	return ([_fileWarningLineNumbers objectForKey:[NSNumber numberWithUnsignedInteger:ln]] != nil);
}
- (WCFileWarning *)fileWarningAtLineNumber:(NSUInteger)ln; {
	return [_fileWarningLineNumbers objectForKey:[NSNumber numberWithUnsignedInteger:ln]];
}
#pragma mark Saving
- (void)saveTextFile; {
	NSError *error = nil;
	if (![[[self fileTextStorage] string] writeToFile:[self absoluteFilePath] atomically:YES encoding:[self fileEncoding] error:&error]) {
		// TODO: report the error
		return;
	}
	
	// reset the change count to 0 since the file was just saved
	[self setFileChangeCount:0];
	// update our file attributes
	[self setFileAttributes:[[NSFileManager defaultManager] fileAttributesAtPath:[self absoluteFilePath] traverseLink:NO]];
}

- (void)saveTextFileAsAndCloseIfCancelled:(BOOL)closeIfCancelled; {
	NSSavePanel *panel = [NSSavePanel savePanel];
	
	[panel setRequiredFileType:[self fileExtension]];
	
	switch ([panel runModalForDirectory:[self folderPath] file:nil]) {
			// save the file with the different name
		case NSOKButton:
			[self setAbsoluteFilePath:[panel filename]];
			[self saveTextFile];
			return;
			// remove all tab view items displaying this text file
		case NSCancelButton:
			if (closeIfCancelled)
				[[self project] removeAllTabViewItemsForTextFile:self];
			return;
		default:
			break;
	}
}

- (void)resetTextFile; {
	[self setFileChangeCount:0];
	[[self fileUndoManager] removeAllActions];
	[_fileTextStorage release];
	_fileTextStorage = nil;
}

- (void)revertTextFile; {
	NSString *string = [NSString stringWithContentsOfFile:[self absoluteFilePath] encoding:[self fileEncoding] error:NULL];
	if (!string) {
		NSStringEncoding encoding;
		string = [NSString stringWithContentsOfFile:[self absoluteFilePath] usedEncoding:&encoding error:NULL];
		if (string)
			_fileEncoding = encoding;
	}
	[[self fileTextStorage] replaceCharactersInRange:[[self fileTextStorage] wholeRange] withString:string];
	[self setFileAttributes:[[NSFileManager defaultManager] fileAttributesAtPath:[self absoluteFilePath] traverseLink:NO]];
}

- (void)validateTextFile; {
	// if the file isn't valid then it's been moved or deleted
	if (![self isValid]) {
		NSString *message = [NSString stringWithFormat:NSLocalizedString(@"\"%@\" Has Been Moved",@"\"%@\" Has Been Moved"), [self name]];
		NSString *format = [NSString stringWithFormat:NSLocalizedString(@"The file for the document that was located at \"%@\" has disappeared. Do you want to re-save the document or close it?",@"file has moved body text"), [self absoluteFilePath]];
		NSAlert *alert = [NSAlert alertWithMessageText:message defaultButton:NSLocalizedString(@"Re-save",@"Re-save") alternateButton:NSLocalizedString(@"Close",@"Close") otherButton:[NSString stringWithFormat:NSLocalizedString(@"Save As%C",@"Save as with ellipsis"), kUnicodeCharacterEllipsis] informativeTextWithFormat:format];
		
		[alert setAlertStyle:NSCriticalAlertStyle];
		
		switch ([alert runModal]) {
				// save the text file
			case NSAlertDefaultReturn:
				[self saveTextFile];
				return;
				// remove all tab view items displaying this text file
			case NSAlertAlternateReturn:
				[[self project] removeAllTabViewItemsForTextFile:self];
				return;
				// allow the user to save the text file with a different name
			case NSAlertOtherReturn:
				[self saveTextFileAsAndCloseIfCancelled:YES];
				return;
			default:
				break;
		}
	}
	
	// check the modification date of the file
	if (!NSUserDefaultsBoolForKey(kWCPrefsFilesCheckIfFilesHaveBeenUpdatedByAnotherApplicationKey))
		return;
	
	NSDictionary *attributes = [[NSFileManager defaultManager] fileAttributesAtPath:[self absoluteFilePath] traverseLink:NO];
	// if the mod date is nil then the file has been remove or renamed and we shouldn't even reach this code path, but just in case
	if (![attributes fileModificationDate])
		return;
	
	// if the dates are equal we are done
	if ([[[self fileAttributes] fileModificationDate] isEqualToDate:[attributes fileModificationDate]])
		return;
	
	// otherwise the file has been modified by another application
	// check to see if we should update the file without asking the user
	if (NSUserDefaultsBoolForKey(kWCPrefsFilesAutomaticallyUpdateChangedFilesKey)) {
		[self revertTextFile];
		return;
	}
	
	NSString *message = [NSString stringWithFormat:NSLocalizedString(@"The file \"%@\" has been updated by another application",@"The file \"%@\" has been updated by another application"), [self name]];
	NSString *format = NSLocalizedString(@"Do you want to ignore the updates the other application has made or reload the file?",@"Do you want to ignore the updates the other application has made or reload the file?");
	NSAlert *alert = [NSAlert alertWithMessageText:message defaultButton:NSLocalizedString(@"Reload",@"Reload") alternateButton:NSLocalizedString(@"Ignore",@"Ignore") otherButton:nil informativeTextWithFormat:format];
	
	[alert setAlertStyle:NSCriticalAlertStyle];
	
	switch ([alert runModal]) {
		case NSAlertDefaultReturn:
			[self revertTextFile];
			break;
		case NSAlertAlternateReturn:
		default:
			break;
	}
}
#pragma mark Accessors
@synthesize fileEncoding=_fileEncoding, fileChangeCount=_fileChangeCount,lineStartIndexes=_lineStartIndexes,fileSelectedRange=_fileSelectedRange,fileAttributes=_fileAttributes,separateEditorWindowFrameString=_separateEditorWindowFrameString;

- (NSDictionary *)fileAttributes {
	if (!_fileAttributes) {
		_fileAttributes = [[[NSFileManager defaultManager] fileAttributesAtPath:[self absoluteFilePath] traverseLink:NO] retain];
	}
	return _fileAttributes;
}

@dynamic textViewForReplace;
- (WCTextView *)textViewForReplace {
	if (!_scrollViewWithTextViewForReplace) {
		_scrollViewWithTextViewForReplace = [[[WCInterfacePerformer sharedInstance] scrollViewWithTextViewForTextFile:self inProject:[self project]] retain];
	}
	return [_scrollViewWithTextViewForReplace documentView];
}

@dynamic numberOfFileSymbols;
- (NSUInteger)numberOfFileSymbols {
	return [[self fileSymbolsArray] count];
}

@dynamic rebuildFileSymbols;
- (BOOL)rebuildFileSymbols {
	return _WCTextFileFlags.rebuildFileSymbols;
}
- (void)setRebuildFileLabels:(BOOL)flag {
	_WCTextFileFlags.rebuildFileSymbols = flag;
}

@dynamic fileContents;
- (NSString *)fileContents {
	if (_fileTextStorage == nil) {
		NSString *string = [NSString stringWithContentsOfFile:[self absoluteFilePath] encoding:NSUTF8StringEncoding error:NULL];
		return (string == nil)?@"":string;
	}
	return [_fileTextStorage string];
}

- (void)_buildFileSymbols {
	_WCTextFileFlags.rebuildFileSymbols = NO;
	
	[_fileLabels removeAllObjects];
	[_fileEquates removeAllObjects];
	[_fileMacros removeAllObjects];
	[_fileDefines removeAllObjects];
	[_fileLabelsArray removeAllObjects];
	[_fileEquatesArray removeAllObjects];
	[_fileSymbolsArray removeAllObjects];
	
	NSString *string = [self fileContents];
	
	extern char *yytext;
	NSUInteger poscounter = 0;
	NSUInteger token = 0;
	
	yystatereset();
	yy_scan_string([string UTF8String]);
	
	RKEnumerator *enumerator = [[[RKEnumerator alloc] initWithRegex:multilineCommentsRegex string:string] autorelease];
	NSMutableArray *commentRanges = [NSMutableArray array];
	while ([enumerator nextRanges] != NULL) {
		[commentRanges addObject:[NSValue valueWithRange:*[enumerator currentRanges]]];
	}
	
	while (token = yylex()) {
		NSString *label = [NSString stringWithUTF8String:yytext];
		NSRange range = NSMakeRange(poscounter, [label length]);
		
		poscounter += range.length;
		
		// skip if its not a special token
		if (token == 1)
			continue;
		 
		// check to see if we are inside a block comment
		BOOL inBlockComment = NO;
		for (NSValue *value in commentRanges) {
			if (NSLocationInRange(range.location, [value rangeValue])) {
				inBlockComment = YES;
				break;
			}
		}
		
		// only add the symbol if we aren't inside a block comment
		if (!inBlockComment) {
			if (token == 2) {
				[_fileLabels addObject:label];
				WCSymbol *symbol = [WCSymbol symbolWithSymbolType:WCSTypeLabel name:label file:self lineNumber:[self lineNumberForCharacterIndex:range.location]];
				[_fileLabelsArray addObject:symbol];
				[_fileSymbolsArray addObject:symbol];
			}
			else if (token == 3) {
				label = [label stringByTrimmingCharactersInSet:equateCharacterSet];
				[_fileEquates addObject:label];
				WCSymbol *symbol = [WCSymbol symbolWithSymbolType:WCSTypeEquate name:label file:self lineNumber:[self lineNumberForCharacterIndex:range.location]];
				[_fileEquatesArray addObject:symbol];
				[_fileSymbolsArray addObject:symbol];
			}
			else if (token == 4) {
				// we want the part after the #macro
				label = [[[label stringByTrimmingCharactersInSet:whiteSpaceCharacterSet] substringFromIndex:6] stringByTrimmingCharactersInSet:whiteSpaceCharacterSet];
				[_fileMacros addObject:label];
			}
			else if (token == 5) {
				// we want the part after the #define
				label = [[[label stringByTrimmingCharactersInSet:whiteSpaceCharacterSet] substringFromIndex:7] stringByTrimmingCharactersInSet:whiteSpaceCharacterSet];
				[_fileDefines addObject:label];
			}
		}
	}
}

@dynamic fileLabels;
- (NSSet *)fileLabels {
	if (_WCTextFileFlags.rebuildFileSymbols) {
		[self _buildFileSymbols];
	}
	return _fileLabels;
}

@dynamic fileLabelsArray;
- (NSArray *)fileLabelsArray {
	if (_WCTextFileFlags.rebuildFileSymbols) {
		[self _buildFileSymbols];
	}
	
	return _fileLabelsArray;
}

@dynamic fileEquates;
- (NSSet *)fileEquates {
	if (_WCTextFileFlags.rebuildFileSymbols) {
		[self _buildFileSymbols];
	}
	return _fileEquates;
}

@dynamic fileEquatesArray;
- (NSArray *)fileEquatesArray {	
	if (_WCTextFileFlags.rebuildFileSymbols) {
		[self _buildFileSymbols];
	}
	return _fileEquatesArray;
}

@dynamic fileSymbolsArray;
- (NSArray *)fileSymbolsArray {	
	if (_WCTextFileFlags.rebuildFileSymbols) {
		[self _buildFileSymbols];
	}
	return _fileSymbolsArray;
}

@dynamic fileDefines;
- (NSSet *)fileDefines {
	if (_WCTextFileFlags.rebuildFileSymbols) {
		[self _buildFileSymbols];
	}
	return _fileDefines;
}

@dynamic fileMacros;
- (NSSet *)fileMacros {
	if (_WCTextFileFlags.rebuildFileSymbols) {
		[self _buildFileSymbols];
	}
	return _fileMacros;
}

@dynamic fileTextStorage;
- (NSTextStorage *)fileTextStorage {
	if (!_fileTextStorage) {
		NSString *string = [NSString stringWithContentsOfFile:[self absoluteFilePath] encoding:[self fileEncoding] error:NULL];
		// the string failed to open with the encoding we have stored, try again and have NSString determine the encoding when it creates the string
		if (!string) {
			NSStringEncoding encoding = 0;
			string = [NSString stringWithContentsOfFile:[self absoluteFilePath] usedEncoding:&encoding error:NULL];
			[self setFileEncoding:encoding];
		}
		_fileTextStorage = [[NSTextStorage alloc] initWithString:string];
		
		[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(_didProcessEditingNotification:) name:NSTextStorageDidProcessEditingNotification object:_fileTextStorage];
	}
	return _fileTextStorage;
}

@dynamic fileUndoManager;
- (NSUndoManager *)fileUndoManager {
	if (!_fileUndoManager) {
		_fileUndoManager = [[NSUndoManager alloc] init];
		
		[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(_willCloseUndoGroupNotification:) name:NSUndoManagerWillCloseUndoGroupNotification object:_fileUndoManager];
		[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(_didUndoChangeNotification:) name:NSUndoManagerDidUndoChangeNotification object:_fileUndoManager];
		[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(_didRedoChangeNotification:) name:NSUndoManagerDidRedoChangeNotification object:_fileUndoManager];
	}
	return _fileUndoManager;
}

@dynamic fileChangeCount;
- (NSInteger)fileChangeCount {
	return _fileChangeCount;
}

- (void)setFileChangeCount:(NSInteger)value {
	if (!value ||
		(!_fileChangeCount && value) ||
		(_fileChangeCount && !value)) {
		
		[self willChangeValueForKey:@"icon"];
		_fileChangeCount = value;
		[self didChangeValueForKey:@"icon"];
		
		[[NSNotificationCenter defaultCenter] postNotificationName:kWCTFFileChangeCountDidChangeNotification object:self];
	}
	else
		_fileChangeCount = value;
}

@dynamic isEdited;
- (BOOL)isEdited {
	return (_fileChangeCount == 0)?NO:YES;
}

@dynamic numberOfFileErrors;
- (NSNumber *)numberOfFileErrors {
	return ([_fileErrorLineNumbers count] == 0)?nil:NSNumberWithUnsignedInteger([_fileErrorLineNumbers count]);
}

@dynamic numberOfFileWarnings;
- (NSNumber *)numberOfFileWarnings {
	return ([_fileWarningLineNumbers count] == 0)?nil:NSNumberWithUnsignedInteger([_fileWarningLineNumbers count]);
}
/*
- (NSNumber *)objectCount {
	NSUInteger total = [_fileErrorLineNumbers count] + [_fileWarningLineNumbers count];
	return (total == 0)?nil:NSNumberWithUnsignedInteger(total);
}
 */

@dynamic totalErrorsAndWarnings;
- (NSUInteger)totalErrorsAndWarnings {
	return ([[self numberOfFileErrors] unsignedIntegerValue] + [[self numberOfFileWarnings] unsignedIntegerValue]);
}
#pragma mark Creation
+ (id)textFileWithFilePath:(NSString *)filePath fileEncoding:(NSStringEncoding)encoding inProject:(WCProject *)project; {
	return [[[[self class] alloc] initWithFilePath:filePath fileEncoding:encoding inProject:project] autorelease];
}

- (id)initWithFilePath:(NSString *)filePath fileEncoding:(NSStringEncoding)encoding inProject:(WCProject *)project; {
	if (!(self = [super initWithFilePath:filePath inProject:project]))
		return nil;
	
	_fileEncoding = encoding;
	_project = project;
	
	_fileLabels = [[NSMutableSet alloc] init];
	_fileEquates = [[NSMutableSet alloc] init];
	_fileMacros = [[NSMutableSet alloc] init];
	_fileDefines = [[NSMutableSet alloc] init];
	_fileLabelsArray = [[NSMutableArray alloc] init];
	_fileEquatesArray = [[NSMutableArray alloc] init];
	_fileSymbolsArray = [[NSMutableArray alloc] init];
	
	_WCTextFileFlags.rebuildFileSymbols = YES;
	
	return self;
}

#pragma mark *** Private Methods ***
- (void)_calculateLines; {
	if (!_lineStartIndexes) {
		_lineStartIndexes = [[NSMutableArray alloc] init];
	}
	
	[_lineStartIndexes removeAllObjects];
	
	unsigned        index, stringLength, lineEnd, contentEnd;
	NSString        *text;
	
	text = [self fileContents];
	stringLength = [text length];
	
	index = 0;
	
	do
	{
		[_lineStartIndexes addObject:[NSNumber numberWithUnsignedInt:index]];
		
		index = NSMaxRange([text lineRangeForRange:NSMakeRange(index, 0)]);
	}
	while (index < stringLength);
	
	// Check if text ends with a new line.
	[text getLineStart:NULL end:&lineEnd contentsEnd:&contentEnd forRange:NSMakeRange([[_lineStartIndexes lastObject] unsignedIntValue], 0)];
	if (contentEnd < lineEnd)
	{
		[_lineStartIndexes addObject:[NSNumber numberWithUnsignedInt:index]];
	}
}

- (void)_calculateLinesStartingAtLine:(unsigned)line; {
	/*
	if (!_lineStartIndexes) {
		_lineStartIndexes = [[NSMutableArray alloc] init];
	}
	 */
	
	[_lineStartIndexes removeObjectsAtIndexes:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(line, [_lineStartIndexes count]-line)]];
	
	unsigned        index, stringLength, lineEnd, contentEnd;
	NSString        *text;
	
	text = [self fileContents];
	stringLength = [text length];
	
	index = ([_lineStartIndexes count] == 0)?0:NSMaxRange([text lineRangeForRange:NSMakeRange([[_lineStartIndexes objectAtIndex:line-1] unsignedIntValue], 0)]);
	
	do {
		[_lineStartIndexes addObject:[NSNumber numberWithUnsignedInt:index]];
		
		index = NSMaxRange([text lineRangeForRange:NSMakeRange(index, 0)]);
	} while (index < stringLength);
	
	[text getLineStart:NULL end:&lineEnd contentsEnd:&contentEnd forRange:NSMakeRange([[_lineStartIndexes lastObject] unsignedIntValue], 0)];
	if (contentEnd < lineEnd) {
		[_lineStartIndexes addObject:[NSNumber numberWithUnsignedInt:index]];
	}
}

#pragma mark Notifications
- (void)_willCloseUndoGroupNotification:(NSNotification *)note {
	[self setFileChangeCount:_fileChangeCount+1];
}

- (void)_didUndoChangeNotification:(NSNotification *)note {
	[self setFileChangeCount:_fileChangeCount-1];
}

- (void)_didRedoChangeNotification:(NSNotification *)note {
	[self setFileChangeCount:_fileChangeCount+1];
}

- (void)_didProcessEditingNotification:(NSNotification *)note {
	NSRange editedRange = [_fileTextStorage editedRange];
	
	if (editedRange.length != 0) {		
		NSString *string = [_fileTextStorage string];
		NSString *searchString = [string substringWithRange:[string lineRangeForRange:editedRange]];
		NSRange newlineRange = [searchString rangeOfCharacterFromSet:[NSCharacterSet newlineCharacterSet]];
		
		if (newlineRange.location != NSNotFound) {
			// only recompute the indices from this index until the end of the document
			// everything above the line is not affected
			unsigned firstLine = [self lineNumberForCharacterIndex:editedRange.location];
			
			[self _calculateLinesStartingAtLine:firstLine];
		}
	}
	else if (editedRange.length == 0) {
		// only recompute the indices from this index until the end of the document
		// everything above the line is not affected
		unsigned firstLine = [self lineNumberForCharacterIndex:editedRange.location];
		
		[self _calculateLinesStartingAtLine:firstLine];
	}
	else {
		[_lineStartIndexes release];
		_lineStartIndexes = nil;
	}
	
	_WCTextFileFlags.rebuildFileSymbols = YES;
	
	[[self project] setShouldRecacheProjectLabels:YES];
	[[self project] setShouldRecacheProjectEquates:YES];
	[[self project] setShouldRecacheProjectDefines:YES];
	[[self project] setShouldRecacheProjectMacros:YES];
	[[self project] setShouldRecacheProjectSymbolsArray:YES];
	
	if ([[[self project] currentTextFile] isEqualTo:self])
		[[self project] setCurrentTextFileSymbols:[self fileSymbolsArray]];
	
	for (NSObject <WCTabViewContext> *context in [[self project] allTabViewContexts]) {
		if (![[context currentTextFile] isEqualTo:self])
			[[WCSyntaxHighlighter sharedInstance] highlightTextView:[context currentTextView]];
	}
}
@end
